﻿using System;
using System.Collections.Generic;
using FStore.Errors;

namespace FStore.Utils
{
    public static partial class Unique
    {
        private static Dictionary<int, HashSet<int>> randomInt32Numbers = new Dictionary<int, HashSet<int>>();
        private static Random Int32Random = new Random();

        /// <summary>
        /// Reserves a domain for the random int32 context
        /// </summary>
        /// <param name="domain">The domain to reserve</param>
        public static void ReserveInt32RandomDomain(int domain)
        {
            // Check the domain is >= 0
            ErrorHelper.GenInvaildParam("domain", domain, domain >= 0, DOMAIN_GT_CONDITION);

            // Check the domain hasn't been reserved already
            ErrorHelper.GenInvaildParam("domain", domain, !randomInt32Numbers.ContainsKey(domain),
                "Domain of {0} already reserved for Int32Random context".UFormat(domain));

            // Reserve the domain
            randomInt32Numbers.Add(domain, new HashSet<int>());
        }

        /// <summary>
        /// Unreserves a domain from the random int32 context
        /// </summary>
        /// <param name="domain">The domain to unreserve</param>
        public static void UnreserveInt32RandomDomain(int domain)
        {
            // Check the domain is >= 0
            ErrorHelper.GenInvaildParam("domain", domain, domain >= 0, DOMAIN_GT_CONDITION);

            // Check the domain is reserved already
            ErrorHelper.GenInvaildParam("domain", domain, randomInt32Numbers.ContainsKey(domain),
                "Domain of {0} isn't registered for Int32Random context".UFormat(domain));

            // Unreserve the domain
            randomInt32Numbers.Remove(domain);
        }

        /// <summary>
        /// Gets a random Int32 for a given domain
        /// </summary>
        /// <param name="domain">The domain for the Int32 random context</param>
        /// <returns>The reserved Int32</returns>
        public static int GetRandomInt32(int domain)
        {
            // Check the domain is >= 0
            ErrorHelper.GenInvaildParam("domain", domain, domain >= 0, DOMAIN_GT_CONDITION);

            // Check the domain has been reserved
            ErrorHelper.GenInvaildParam("domain", domain, randomInt32Numbers.ContainsKey(domain),
                "Domain of {0} isn't registered for Int32Random context".UFormat(domain));

            // Check this domain hasn't run out of allocations
            ErrorHelper.GenError(randomInt32Numbers[domain].Count < int.MaxValue,
                "Domain of {0} has run out of allocations".UFormat(domain));

            // If we only have one allocation left then we know it's int.MaxValue - 1

            if (randomInt32Numbers[domain].Count == (int.MaxValue - 2))
            {
                // Save the known value and return it
                randomInt32Numbers[domain].Add(int.MaxValue - 1);
                return int.MaxValue - 1;
            }

            // Generate a random number
            int randomNumber = Int32Random.Next(0, int.MaxValue);

            while (randomInt32Numbers[domain].Contains(randomNumber))
            {
                randomNumber = Int32Random.Next(0, int.MaxValue);
            }

            // Save and return the number
            randomInt32Numbers[domain].Add(randomNumber);
            return randomNumber;
        }

        /// <summary>
        /// Removes a random Int32 from a given domain
        /// </summary>
        /// <param name="num">The number to remove</param>
        /// <param name="domain">The domain to remove it from</param>
        public static void RemoveRandomInt32(int num, int domain)
        {
            // Check the domain is >= 0
            ErrorHelper.GenInvaildParam("domain", domain, domain >= 0, DOMAIN_GT_CONDITION);

            // Check the domain has been reserved
            ErrorHelper.GenInvaildParam("domain", domain, randomInt32Numbers.ContainsKey(domain),
                "Domain of {0} isn't registered for Int32Random context".UFormat(domain));

            // Check the number exists in the domain
            ErrorHelper.GenInvaildParam("num", num, randomInt32Numbers[domain].Contains(num),
                "Number of {0} not registered to domain {1}".UFormat(num, domain));

            // Remove the number
            randomInt32Numbers[domain].Remove(num);
        }

        /// <summary>
        /// Determines whether a domain for the random Int32 context is reserved
        /// </summary>
        /// <param name="domain">The domain to check</param>
        /// <returns>
        /// <c>True</c> if the domain is reserved, <c>False</c> otherwise
        /// </returns>
        public static bool IsDomainReservedRandomInt32(int domain)
        {
            return randomInt32Numbers.ContainsKey(domain);
        }

        /// <summary>
        /// Determines whether a random int32 has been registered with a given domain
        /// </summary>
        /// <param name="num">The number to check for</param>
        /// <param name="domain">The domain to check</param>
        /// <returns>
        /// <c>True</c> if the domain is reserved, <c>False</c> otherwise
        /// </returns>
        public static bool IsRandomInt32RegisteredForDomain(int num, int domain)
        {
            // Check the domain is >= 0
            ErrorHelper.GenInvaildParam("domain", domain, domain >= 0, DOMAIN_GT_CONDITION);

            // Check the domain has been reserved
            ErrorHelper.GenInvaildParam("domain", domain, randomInt32Numbers.ContainsKey(domain),
                "Domain of {0} isn't registered for Int32Random context".UFormat(domain));

            return randomInt32Numbers[domain].Contains(num);
        }

        /// <summary>
        /// Allows a number of external origin to be reserved
        /// </summary>
        /// <param name="num">The number to reserve</param>
        /// <param name="domain">The domain to reserve it in</param>
        internal static void TryExternalReserve(int num, int domain)
        {
            // Check the domain is >= 0
            ErrorHelper.GenInvaildParam("domain", domain, domain >= 0, DOMAIN_GT_CONDITION);

            // Check the domain has been reserved
            ErrorHelper.GenInvaildParam("domain", domain, randomInt32Numbers.ContainsKey(domain),
                "Domain of {0} isn't registered for Int32Random context".UFormat(domain));

            // Check the number exists in the domain
            ErrorHelper.GenInvaildParam("num", num, randomInt32Numbers[domain].Contains(num) == false,
                "Number of {0} already in domain {1}".UFormat(num, domain));

            // We know the number isn't reserved already, so just take it
            randomInt32Numbers[domain].Add(num);
        }
    }
}